# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import annotations

import datetime as dt
import functools
import logging
import os
import shutil
from typing import Optional, Type

from typing_extensions import override

from crossbench import path as pth
from crossbench.plt.base import Platform
from crossbench.plt.signals import WinSignals
from crossbench.plt.version import PlatformVersion


class WinVersion(PlatformVersion):
  pass


class WinPlatform(Platform):
  # TODO: support remote platforms
  SEARCH_PATHS = (
      pth.LocalPath("."),
      pth.LocalPath(os.path.expandvars("%ProgramFiles%")),
      pth.LocalPath(os.path.expandvars("%ProgramFiles(x86)%")),
      pth.LocalPath(os.path.expandvars("%APPDATA%")),
      pth.LocalPath(os.path.expandvars("%LOCALAPPDATA%")),
  )

  @property
  def signals(self) -> Type[WinSignals]:
    return WinSignals

  @property
  @override
  def is_win(self) -> bool:
    return True

  @property
  @override
  def name(self) -> str:
    return "win"

  @property
  @override
  def model(self) -> str:
    # TODO: implement
    return ""

  def cmd_stdout(self, *args, **kwargs) -> str:
    cmd = ["cmd", "/c", *args]
    return self.sh_stdout(*cmd, **kwargs)

  def powershell_stdout(self, *args, **kwargs) -> str:
    cmd = ["powershell", "-c", *args]
    return self.sh_stdout(*cmd, **kwargs)

  @functools.cached_property
  @override
  def version_str(self) -> str:
    return self.cmd_stdout("ver").strip()

  @functools.cached_property
  @override
  def version(self) -> WinVersion:
    return WinVersion.parse(self.version_str)

  @functools.cached_property
  @override
  def cpu(self) -> str:
    return self.powershell_stdout(
        "Get-CIMInstance -query 'select * from Win32_Processor' | ft Name"
    ).strip().splitlines()[2].strip()

  @functools.lru_cache(maxsize=1)
  @override
  def _raw_machine_arch(self) -> str:
    self.assert_is_local()
    # The method in base class doesn't always give the correct answer,
    # because it uses py_platform.machine, which give the architecture of
    # the Python binary. It is possible to run x64 Python on ARM Windows.
    cpu_caption = self.powershell_stdout(
        "Get-CIMInstance -query 'select * from Win32_Processor' | ft Caption"
    ).strip().splitlines()[2].strip().lower()
    if cpu_caption.startswith("arm"):
      return "arm64" if "64-bit" in cpu_caption else "arm"
    return super()._raw_machine_arch()

  @override
  def uptime(self) -> dt.timedelta:
    """Parse powershell last boot time time-span into a timedelta object.
    Example Output:
    Days              : 14
    Hours             : 2
    Minutes           : 19
    Seconds           : 54
    Milliseconds      : 978
    Ticks             : 12179949789862
    TotalDays         : 14.0971641086366
    TotalHours        : 338.331938607278
    TotalMinutes      : 20299.9163164367
    TotalSeconds      : 1217994.9789862
    TotalMilliseconds : 1217994978.9862
    """
    uptime_output = self.powershell_stdout(
        "(New-TimeSpan -Start ("
        "Get-CimInstance Win32_OperatingSystem).LastBootUpTime"
        ")")
    results = {}
    for line in uptime_output.splitlines():
      line = line.strip()
      if not line:
        continue
      unit, value = line.split(":", maxsplit=1)
      unit = unit.strip()
      value_f: float = float(value)
      results[unit] = value_f
    return dt.timedelta(
        days=results["Days"],
        hours=results["Hours"],
        minutes=results["Minutes"],
        seconds=results["Seconds"],
        milliseconds=results["Milliseconds"])

  @override
  def search_binary(self, app_or_bin: pth.AnyPathLike) -> Optional[pth.AnyPath]:
    self.assert_is_local()
    app_or_bin_path: pth.AnyPath = self.path(app_or_bin)
    if not app_or_bin_path.parts:
      raise ValueError("Got empty path")
    if app_or_bin_path.suffix.lower() not in (".exe", ".bat"):
      raise ValueError("Expected executable path with '.exe' or '.bat' suffix, "
                       f"but got: '{app_or_bin_path.name}'")
    if result_path := self.which(app_or_bin):
      assert self.exists(result_path), f"{result_path} does not exist."
      return result_path
    for path in self.SEARCH_PATHS:
      # Recreate Path object for easier pyfakefs testing
      result_path = self.path(path) / app_or_bin
      if self.exists(result_path):
        return result_path
    return None

  @override
  def app_version(self, app_or_bin: pth.AnyPathLike) -> str:
    app_or_bin = self.path(app_or_bin)
    if not self.exists(app_or_bin):
      raise ValueError(f"Binary {app_or_bin} does not exist.")
    if version := self.sh_stdout(
        "powershell", "-command",
        f"(Get-Item '{app_or_bin}').VersionInfo.ProductVersion").strip():
      name = self.sh_stdout(
          "powershell", "-command",
          f"(Get-Item '{app_or_bin}').VersionInfo.ProductName").strip()
      return f"{name} {version}"
    try:
      # Fall back to command-line tools.
      if version := self.sh_stdout(app_or_bin, "--version").strip():
        return version
    except Exception as e:  # noqa: BLE001
      logging.debug("Failed to extract binary tool version: %s", e)
    raise ValueError(f"Could not extract version for {app_or_bin}")

  @override
  def symlink_or_copy(self, src: pth.AnyPathLike,
                      dst: pth.AnyPathLike) -> pth.AnyPath:
    """Windows does not support symlinking without admin support.
    Copy files on windows but symlink everywhere else (see base Platform)."""
    self.assert_is_local()
    dst_path = self.path(dst)
    shutil.copy(os.fspath(self.path(src)), os.fspath(dst_path))
    return dst_path
